package com.example.gulimall.product.service.impl;

import cn.hutool.core.lang.TypeReference;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.common.constant.ProductConstant;
import com.example.common.utils.PageUtils;
import com.example.common.utils.Query;
import com.example.gulimall.product.dao.CategoryDao;
import com.example.gulimall.product.entity.CategoryEntity;
import com.example.gulimall.product.service.CategoryBrandRelationService;
import com.example.gulimall.product.service.CategoryService;
import com.example.gulimall.product.vo.CateLog2Vo;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {


    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;
    @Autowired private StringRedisTemplate redisTemplate;
    @Autowired private RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

//    @Override
//    public List<CategoryEntity> tree() {
//        List<CategoryEntity> list = this.baseMapper.selectList(Wrappers.lambdaQuery());
//
//        // 寻找一级分类
//        return list.stream()
//                .filter(a -> a.getParentCid() == 0)
//                .peek(b -> b.setChildren(getChildren(b, list)))
//                .sorted(
//                        (a,b) -> (Objects.isNull(a.getSort()) ? 0 : a.getSort())
//                                - (Objects.isNull(a.getSort()) ? 0 : b.getSort())
//                )
//                .collect(Collectors.toList());
//    }
//
//
//    private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
//
//        return all.stream()
//                .filter(a -> Objects.equals(a.getParentCid(), root.getParentCid()))
//                .peek(b -> b.setChildren(getChildren(b, all)))
//                .sorted(
//                        (a,b) -> (Objects.isNull(a.getSort()) ? 0 : a.getSort())
//                                - (Objects.isNull(a.getSort()) ? 0 : b.getSort())
//                )
//                .collect(Collectors.toList());
//    }


    @Override
    public List<CategoryEntity> tree() {

        List<CategoryEntity> entities = this.baseMapper.selectList(Wrappers.lambdaQuery());

        Map<Long, CategoryEntity> mapData = new HashMap<>();
        List<CategoryEntity> rootData = new ArrayList<>();


        for (CategoryEntity item : entities) {
            // 全部放入map中
            if (item.getParentCid() == 0) {
                rootData.add(item);
            }
            mapData.put(item.getCatId(), item);
        }

        rootData = rootData.parallelStream().sorted(
                Comparator.comparingInt(CategoryEntity::getSort)
        ).collect(Collectors.toList());

        for (CategoryEntity item : entities) {

            // 一次性把所有列表组合成树
            Long parentCid = item.getParentCid();
            // 排除根节点
            if (parentCid == 0) {
                continue;
            }

            CategoryEntity parent = mapData.get(parentCid);
            if (parent.getChildren() == null) {
                parent.setChildren(new ArrayList<>());
            }
            parent.getChildren().add(item);
        }

        for (CategoryEntity rootDatum : rootData) {
            rootDatum.setChildren(
                    rootDatum.getChildren()
                            .parallelStream()
                            .sorted(Comparator.comparingInt(CategoryEntity::getSort))
                            .collect(Collectors.toList())
            );
        }
        return rootData;
    }

    @Override
    @Caching(evict = {
            @CacheEvict(cacheNames = "category", key = "'getCategoryByLevel'"),
            @CacheEvict(cacheNames = "category", key = "'getCateLogJson'")
    })
    public void removeBatchById(List<Long> asList) {
        // TODO 检查当前删除菜单是否已经被应用
        this.baseMapper.deleteBatchIds(asList);
    }

    @Override
    @Caching(evict = {
            @CacheEvict(cacheNames = "category", key = "'getCategoryByLevel'"),
            @CacheEvict(cacheNames = "category", key = "'getCateLogJson'")
    })
    public void updateCascade(CategoryEntity category) {
        this.baseMapper.updateById(category);

        this.categoryBrandRelationService.updateCategory(category);
    }

    @Override
    public Long[] selectCatalogPath(Long catelogId) {
        Long[] ret = new Long[3];
        ret[2] = catelogId;

        ret[1] = this.baseMapper.selectParentPath(catelogId);
        ret[0] = this.baseMapper.selectParentPath(ret[1]);

        return ret;
    }

    @Override
    public List<CategoryEntity> getBatchId(Set<Long> cateLogIds) {
        if (CollectionUtils.isEmpty(cateLogIds)) {
            return Collections.emptyList();
        }
        List<CategoryEntity> entities = this.baseMapper.selectList(
                Wrappers.<CategoryEntity>lambdaQuery()
                        .in(CategoryEntity::getCatId, cateLogIds)
        );
        if (CollectionUtils.isEmpty(entities)) {
            return Collections.emptyList();
        }
        return entities;
    }

    @Override
    @Cacheable(cacheNames = "category", key = "#root.methodName")
    public List<CategoryEntity> getCategoryByLevel(int level, Long parentId) {
        return this.baseMapper.selectList(
                Wrappers.<CategoryEntity>lambdaQuery()
                        .eq(CategoryEntity::getCatLevel, level)
                        .eq(Objects.nonNull(parentId), CategoryEntity::getParentCid, parentId)
        );
    }

    /**
     * 会产生对外内存溢出的问题：
     * 原因：lettuce 使用 netty 作为通讯框架，netty 使用的是堆外内存，
     * 解决方案：不能直接使用 -Dio.netty.maxDirectMemory
     * 1. 升级 lettuce 客户端
     * 2. 使用 jedis 客户端
     */
    @Override
    @Cacheable(cacheNames = "category", key = "#root.methodName")
    public Map<String, List<CateLog2Vo>> getCateLogJson() {

        /*
            缓存穿透：查询一个不存在的数据，例如 ID = -1，缓存不命中，全部走数据库，数据库也没有记录
                解决方案：将 null 值写入缓存
            缓存雪崩：大面积数据同时失效
                解决方案：key 值失效时间使用随机值
            缓存击穿：key 在大量请求的时候实现，那么查询的事情将会落到数据库
                解决方案：加锁（大量并发的时候，只让一个人去查数据库，其他线程等待，等到查询的线程释放了锁后，其他线程就不必再去数据库了）
         */

        /*
        改造：
        1. 空结果缓存，解决缓存穿透
        2. 加上随机过期时间，解决缓存雪崩
        3. 加锁，解决缓存击穿

        加锁：
         1. synchronized 锁：单体项目无法实现分布式锁，只能锁当前服务
         2. redis setNx 分布式锁：需要大量代码加锁，考虑的方面比较多
         3. redisson 框架：

         */
        String cateStr = this.redisTemplate.opsForValue().get(ProductConstant.CATEGORY_REDIS_KEY);
        if (StringUtils.isBlank(cateStr)) {
            Map<String, List<CateLog2Vo>> json = this.getCateLogJsonWithDistributedLockRedisSon();
            // 加上失效时间
            this.redisTemplate.opsForValue()
                    .set(ProductConstant.CATEGORY_REDIS_KEY, JSONObject.toJSONString(json), 1, TimeUnit.DAYS);
            return json;
        }
        // 复杂类型转换
        TypeReference<Map<String, List<CateLog2Vo>>> reference = new TypeReference<Map<String, List<CateLog2Vo>>>() {};
        return JSONObject.parseObject(cateStr, reference);
    }

    public Map<String, List<CateLog2Vo>> getCateLogJsonWithDistributedLockRedisSon(){
        RLock lock = redissonClient.getLock(ProductConstant.CATE_LOG_JSON_DISTRIBUTED_LOCK);
        // 当业务逻辑未执行完毕的时候，会启动定时任务给锁刷新超时时间 30 * 1000 【看门狗】， 30000 / 3 后续期一次
        // lock.lock();
        // 自定义超时时间，那么将不会采用看门狗自动续时，业务推荐使用
        // lock.lock(30, TimeUnit.SECONDS);
        // 尝试1s内获取锁
        // lock.tryLock(1, 30, TimeUnit.SECONDS);
        try {
            // 一旦尝试获取到锁，就返回 true，未获得锁，等待 waitTime
            lock.lock(30, TimeUnit.SECONDS);
            return this.getCateLogJsonFromDb();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 使用 setNx 来设置分布式锁
     * 问题：删锁之前，程序代码出现问题，导致其他线程永远获取不到锁
     * 解决：设置 key 失效时间
     */
    @Deprecated
    public Map<String, List<CateLog2Vo>> getCateLogJsonWithDistributedLockNx() {
        String lockVal = UUID.randomUUID().toString();
        //  1. 抢占分布式锁，设置过期时间 (原子操作)
        Boolean locked = this.redisTemplate.opsForValue().setIfAbsent(ProductConstant.CATE_LOG_JSON_DISTRIBUTED_LOCK, lockVal, 300, TimeUnit.SECONDS);
        // 加锁成功
        if (Objects.nonNull(locked) && locked) {

               /*
                问题1：业务超时，锁已经自动过期，别人已获得锁，自己执行删锁，给别人删了
                解决方案：删除自己的锁，使用 UUID 作为 nx 的 value 来完成
                问题2：当重新获取 value 值时，已经从 redis 中拿到了值，但是由于返回结果的网络开销刚好超过了key的过期时间，
                  其他线程设置了值，那么这里比较的内容却和 redis 中实际的值是不同的 (获取值对比并不是原子操作)，代码如下：
                    String lockValue = this.redisTemplate.opsForValue().get("CateLogJsonWithDistributedLock");
                    if (lockVal.equalsIgnoreCase(lockValue)) {
                        this.redisTemplate.delete("CateLogJsonWithDistributedLock");
                    }
                解决方案：删锁和比较值做成原子操作，采用 lua 脚本
             */

            Map<String, List<CateLog2Vo>> json = Collections.emptyMap();
            try {
                 json = this.getCateLogJsonFromDb();
            } finally {
                // 使用脚本删除
                this.redisTemplate.execute(this.deleteLockScript(), Collections.singletonList(ProductConstant.CATE_LOG_JSON_DISTRIBUTED_LOCK), lockVal);
            }
            return json;
        }
        try {
            // 休眠 100ms 后继续重试
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 加锁失败，重试加锁
        return this.getCateLogJsonWithDistributedLockNx();
    }

    /**
     * TODO zyred 继续优化
     */
    public Map<String, List<CateLog2Vo>> getCateLogJsonFromDb() {

        // 查询出所有的分类
        List<CategoryEntity> all = baseMapper.selectList(Wrappers.lambdaQuery());

        List<CategoryEntity> parent = this.getCategoryList(all, 0L);
        return parent.stream().collect(Collectors.toMap(
                k -> String.valueOf(k.getCatId()),
                v -> {
                    List<CategoryEntity> child_2 = this.getCategoryList(all, v.getCatId());
                    List<CateLog2Vo> log2VoList = null;
                    if (CollectionUtils.isNotEmpty(child_2)) {
                        log2VoList = child_2.stream().map(
                                ch2 -> {
                                    CateLog2Vo vo2 = new CateLog2Vo(String.valueOf(v.getCatId()), null, ch2.getName(), String.valueOf(ch2.getCatId()));
                                    List<CategoryEntity> child_3 = this.getCategoryList(all, ch2.getCatId());
                                    List<CateLog2Vo.CateLog3Vo> child_3_vo = null;
                                    if (CollectionUtils.isNotEmpty(child_3)) {
                                        child_3_vo = child_3.stream().map(
                                                ch3 -> new CateLog2Vo.CateLog3Vo(String.valueOf(ch2.getCatId()), ch3.getName(), String.valueOf(ch3.getCatId()))
                                        ).collect(Collectors.toList());
                                    }
                                    vo2.setCatelog3List(CollectionUtils.isNotEmpty(child_3_vo) ? child_3_vo : Collections.emptyList());
                                    return vo2;
                                }
                        ).collect(Collectors.toList());
                    }
                    return CollectionUtils.isNotEmpty(log2VoList) ? log2VoList : Collections.emptyList();
                }
        ));
    }

    /**
     * 优化多次走数据库，从一个集合中获取不同层级的结果
     *
     * @param all       所有的分类
     * @param parentCid 夫分类ID
     * @return          夫分类所属的子分类
     */
    private List<CategoryEntity> getCategoryList (List<CategoryEntity> all, Long parentCid) {
        if (CollectionUtils.isEmpty(all)) {
            return Collections.emptyList();
        }
        // 返回真正的结果
        return all.stream().filter(a -> Objects.requireNonNull(a.getParentCid()).equals(parentCid)).collect(Collectors.toList());
    }

    private DefaultRedisScript<Long> deleteLockScript () {

        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
        return new DefaultRedisScript<>(script, Long.class);
    }
}